
/*
   基于ESP32，ESP8266的GPIO太少
   烤箱改回流焊程序，基于Max6675+K型电偶测温
   将数据以csv格式存放TF卡，
   提供WEB接口，提供实时温度状态，预设温度曲线，以曲线图
   形式展示。
   温控流程：
   1、选择温度曲线配置，读取SD卡，选择默认温度曲线配置；
   2、根据温度曲线加热（PID部分后考虑），过程记录温度数据；
   3、加热结束警报开箱散热。
   4、预计增加显示设备，显示实时温度，曲线设定温度，当前进度，总进度。
   5、内置默认温控曲线，无SD或者SD中无配置，则启用默认配置。
   注：温控曲线支持pid调温，pid将永久性存储在SPIFS中。
   提供两种连接模式，默认为AP模式，可于WEBAPI中设置模式为Client模式，
   联网配置存于中SPIFS中
*/
#include "Arduino.h"
#include <U8g2lib.h>

#include "FS.h"
#include <SPI.h>
#include <SD.h>

#include <Thermocouple.h>
#include <MAX6675_Thermocouple.h>
#include <SmoothThermocouple.h>

#include <WiFi.h>
#include <ArduinoJson.h>
#include <Ticker.h>

#include "AiEsp32RotaryEncoder.h"
//SSD1306
//#define LCD_SDA 21
//#define LCD_SCL 22

//ST7920 12864B LCD
#define LCD_CS 0
#define LCD_RW 2
#define LCD_E 15

//SDReader
#define SD_SS 5
//Max6675
#define SCK_PIN 4
#define CS_PIN 16
#define SO_PIN 17
//Encoder
#define ROTARY_ENCODER_A_PIN 32
#define ROTARY_ENCODER_B_PIN 33
#define ROTARY_ENCODER_BUTTON_PIN 25
#define ROTARY_ENCODER_VCC_PIN 26 /*put -1 of Rotary encoder Vcc is connected directly to 3,3V; else you can use declared output pin for powering rotary encoder */
//SSR
#define SSR_UP_PIN 27
#define SSR_DOWN_PIN 14
//Buzzer
#define BUZ_PIN 12
//Servo
#define SERVO_PIN 13
//Fan GPIO9可能不能使用
#define FAN_PIN 9

//运行时常量
//容许误差温度
const int TEMP_OFFSET = 2;
// csv日志路径
const String LOG_PATH = "/log";
// config路径
const String CONF_PATH = "/conf";
const String CURRENT_CONF = "/conf/oven.conf";
String SID       = "room2";
String PASSWD   = "aidaidai";

//加热任务全局变量
bool running = 0;
int stage = 0;
int maxStage = 0;
int targetTemp = 0;
int stageTimer = 0;
String confName = "";
int counter = 0;
//总时长
int maxTime = 0;

//温度全局变量
double currentTemp = 0;

Ticker mainTicker;
Ticker tempTicker;
Ticker encoderTicker;

//ST7920 u8g2
U8G2_ST7920_128X64_F_SW_SPI u8g2(U8G2_R0, /* clock=*/ 15, /* data=*/ 2, /* CS=*/ 0, /* reset=*/ 16); 

//------------------------通用---------------------------------
//日志记录，暂时输出至串口
void logger(String txt) {
  Serial.println(txt);
}
String timeConverter(int times) {
  if (times > 0) {
    String ms = "";
    int m = times / 60;
    int s = times % 60;
    if (m < 10)
      ms += "0";
    ms += String(m);
    if (s < 10)
      ms += ":0";
    else
      ms += ":";
    ms += String(s);
    return ms;
  } else {
    return "00:00";
  }
}
//------------------------config------------------------------
//{"confName":"first config","stage":[{"timer":50,"temp":220},{"timer":10,"temp":280}]}
StaticJsonDocument<512> config;

void loadConfiguration(const char *filename) {
  File file = SD.open(filename);
  DeserializationError error = deserializeJson(config, file);
  if (error)
    Serial.println(F("Failed to read file, using default configuration"));
  file.close();
}

//------------------------timer------------------------------
const char* ntpServer = "ntp1.aliyun.com";
const long  gmtOffset_sec = 3600;
const int   daylightOffset_sec = 3600;
void NTP_init() {
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
}

struct tm getTime() {
  struct tm timeinfo;
  if (!getLocalTime(&timeinfo)) {
    Serial.println("Failed to obtain time");
  }
  return timeinfo;
}

//---------------------SSD1306--------------------------------
//UI模式，0：主UI，1
int uiMode = 0;
int menuFlag = 0;

void lcd_init() {
  u8g2.begin();
}





String getStageTime() {
  if (!running)
    return "00:00";
  int allTime = counter;
  for (int i = 0; i < stage; i++) {
    allTime += ((int)config["stage"][i]["timer"]);
  }
  return timeConverter(allTime);
}

//------------------------TF Ccard----------------------------
String csvName = "";
void sd_init() {
  if (!SD.begin(SD_SS)) {
    logger("SD Card Module initialization failed!");
    return;
  }
  logger("SD Card Module initialization done.");
  createDir(SD, LOG_PATH.c_str());
}

int getTotalSpace() {
  return (SD.totalBytes() / (1024 * 1024 * 1024));
}
int getUsedSpace() {
  return (SD.usedBytes() / (1024 * 1024 * 1024));
}
// 记录温度日志
void tmpRecord() {
  //csv格式:时间,温度
  struct tm timeinfo;
  getLocalTime(&timeinfo);
  String myFormat = "%Y-%m-%d:%H:%M:%S";
  char strTime[255] = "\0";
  strftime(strTime, sizeof(strTime), myFormat.c_str(), &timeinfo);
  String txt = String(strTime) + "," + String(currentTemp) + "\r\n";
  appendFile(csvName.c_str(), txt.c_str());
}
/**
   主进程开始时预创建CSV文件
*/
void mkCSV() {
  String csvFormat = "task_%Y%m%d_%H%M.csv";
  struct tm timeinfo;
  getLocalTime(&timeinfo);
  char fileName[255] = "\0";
  strftime(fileName, sizeof(fileName), csvFormat.c_str(), &timeinfo);
  csvName = LOG_PATH + "/" + String(fileName);
}

void createDir(fs::FS &fs, const char * path) {
  Serial.printf("Creating Dir: %s\n", path);
  if (fs.mkdir(path)) {
    Serial.println("Dir created");
  } else {
    Serial.println("mkdir failed");
  }
}

void appendFile(const char * path, const char * message) {
  File file = SD.open(path, FILE_APPEND);
  if (!file) {
    Serial.println("Failed to open file for appending");
    return;
  }
  if (!file.print(message)) {
    Serial.println("Append failed");
  }
  file.close();
}


//------------------------Max6675-------------------------------
#define SMOOTHING_FACTOR 5
Thermocouple* thermocouple = NULL;
void thermel_init() {
  Thermocouple* originThermocouple = new MAX6675_Thermocouple(SCK_PIN, CS_PIN, SO_PIN);
  thermocouple = new SmoothThermocouple(originThermocouple, SMOOTHING_FACTOR);
  tempTicker.attach_ms(500, updateTemp);
}

void updateTemp() {
  currentTemp =  thermocouple->readCelsius();
}
//----------------------ENCODER-------------------------------

AiEsp32RotaryEncoder rotaryEncoder = AiEsp32RotaryEncoder(ROTARY_ENCODER_A_PIN, ROTARY_ENCODER_B_PIN, ROTARY_ENCODER_BUTTON_PIN, ROTARY_ENCODER_VCC_PIN);

int test_limits = 2;

void rotary_loop() {
  if (rotaryEncoder.currentButtonState() == BUT_RELEASED) {
    //TODO 按钮
  }
  int16_t encoderDelta = rotaryEncoder.encoderChanged();
  if (encoderDelta == 0) return;
  if (encoderDelta > 0) {
    Serial.print("+");
  }
  if (encoderDelta < 0) {
    Serial.print("-");
  }
}

void encoder_init() {
  rotaryEncoder.begin();
  rotaryEncoder.setBoundaries(0, 10, true);
  encoderTicker.attach_ms(50, rotary_loop);
}

//-------------------------SSR继电器--------------------------
void SSR_init() {
  pinMode(SSR_UP_PIN, OUTPUT);
  pinMode(SSR_DOWN_PIN, OUTPUT);
}
void setSSR(int pos, int stat) {
  digitalWrite(pos, stat);
  logger("SSR set:" + String(stat));
}

//--------------------------BUZZER---------------------------

//BB叫警报，超时时间
void alert() {

}
//停止BB叫
void stopAlert() {

}

//--------------------------FAN------------------------------

void startFan() {

}
void stopFan() {

}

//------------------------MAIN--------------------------------
/**
   开始新任务
   1、读取一个配置文件
   2、进入第一个阶段
   通过定时任务调度器执行任务，每秒执行一次，进行累计时间，超过后进入下一个阶段
*/
void startTask() {
  if (!running) {
    loadConfiguration(CURRENT_CONF.c_str());
    confName = String((const char*)config["confName"]);
    stage = 0;
    targetTemp = config["stage"][stage]["temp"];
    stageTimer = config["stage"][stage]["timer"];
    maxStage = (config["stage"].size() - 1);
    running = 1;
    maxTime = 0;
    //总时长
    for (int i = 0; i < maxStage; i++) {
      maxTime += ((int)config["stage"][i]["timer"]);
    }
    //执行加热调度器
    logger("task start");
    mkCSV();
    mainTicker.attach(1, mainTask);
  }
}
void stopTask() {
  mainTicker.detach();
  confName = "";
  stage = 0;
  targetTemp = 0;
  stageTimer = 0;
  maxStage = 0;
  running = 0;
  maxTime = 0;
  counter = 0;
  logger("task stoped");
}
/**
  调度器主任务
*/
void mainTask() {
  String msg = "stage:" + String(stage) + ";counter:" + String(counter) + ";targetTemp:" + String(targetTemp);
  logger(msg);
  //任务判断，是否有任务执行
  if (running) {
    //记录csv
    tmpRecord();
    //判断任务阶段是否完成
    if (counter >= stageTimer) {
      //判断阶段，有下个阶段就进入下个阶段
      if (stage < (maxStage - 1)) {
        ++stage;
        logger("stage:" + String(stage));
        targetTemp = config["stage"][stage]["temp"];
        stageTimer = config["stage"][stage]["timer"];
      }
      //如果最后阶段并且达到时间，停止调度器。
      else if (stage = maxStage) {
        mainTicker.detach();
        confName = "";
        stage = 0;
        targetTemp = 0;
        stageTimer = 0;
        maxStage = 0;
        running = 0;
        maxTime = 0;
        logger("task finish");
      }
      counter = 0;
    }
    else if (counter <= stageTimer) {
      //当前温度与目标温度对比，不够就加，够就停止
      if (targetTemp > currentTemp && (abs(targetTemp - currentTemp) > TEMP_OFFSET)) {
        setSSR(SSR_UP_PIN, HIGH);
      } else {
        //温度够了就停止加热
        setSSR(SSR_UP_PIN, LOW);
      }
    }
  }
  //计数+1
  ++counter;
}

//---------------------------WIFI------------------------------

void wifi_init() {
  //connect to WiFi
  Serial.printf("Connecting to %s ", SID);
  WiFi.begin(SID.c_str(), PASSWD.c_str());
  int maxtimes = 50;
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    maxtimes--;
    if (maxtimes < 0)
      return;
  }
  Serial.println(" CONNECTED");
}



//-----------------------------------------------------------
void setup() {
  Serial.begin(115200);
  encoder_init();
  lcd_init();
  sd_init();
  thermel_init();
  SSR_init();
  wifi_init();
  NTP_init();

}
// width: 128, height: 64
const unsigned char col[]  = { 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x34,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x00,0x00,0x80,0x1b,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x58,0x00,0x00,0x00,0x00,0x00,0x60,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x00,0x00,0x5c,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x41,0x00,0x00,0x00,0x00,0x00,0x03,0x06,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xc0,0x25,0x00,0x00,0x00,0x00,0x80,0x44,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x20,0x22,0x00,0x00,0x00,0x00,0x60,0x11,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x90,0x32,0x00,0x00,0x00,0x00,0x38,0x04,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x8c,0x22,0x00,0x00,0x00,0x00,0x0c,0x81,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x12,0x25,0x00,0x00,0x00,0x00,0x03,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x8f,0x12,0x00,0x00,0x00,0xc0,0x20,0x60,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x90,0x12,0x00,0x0c,0x00,0x60,0x08,0x48,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x96,0x10,0x80,0x03,0x00,0x38,0x02,0x22,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x44,0x0b,0x60,0x04,0x00,0x0c,0x80,0x20,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xa0,0xaa,0x12,0x58,0x06,0x00,0x96,0x20,0x28,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x30,0xce,0x0a,0xa6,0x02,0x00,0x0b,0x08,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xd8,0xff,0x3f,0x23,0x03,0x80,0x25,0x02,0x0c,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x14,0x85,0xf0,0x6f,0x01,0xc0,0x08,0x81,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x1f,0x08,0x40,0xf9,0x01,0x60,0x25,0x20,0x02,0x00,0x00,0x00,0x00,0x00,0xe0,0x31,0x7c,0x00,0x80,0x40,0x7e,0xb1,0xaa,0x0a,0x04,0x00,0x00,0x00,0x00,0x00,0x10,0x56,0xa0,0x00,0x00,0x80,0x10,0xee,0x5e,0x05,0x03,0x00,0x00,0x00,0x00,0x00,0x88,0x80,0x80,0x44,0x22,0x18,0x40,0x08,0xff,0x28,0x02,0x00,0x00,0x00,0x00,0x00,0x58,0x00,0x11,0x88,0x6c,0x30,0x00,0x00,0x00,0x83,0x02,0x00,0x00,0x00,0x00,0x00,0x2c,0x00,0x29,0x00,0xd1,0x00,0x10,0x02,0xd2,0x02,0x01,0x00,0x00,0x00,0x00,0x00,0x34,0x03,0x00,0x02,0x1b,0x20,0x00,0x80,0xa8,0x04,0x01,0x00,0x00,0x00,0x00,0x00,0x9c,0x03,0x02,0x02,0x12,0x00,0x00,0x40,0xba,0xd0,0x00,0x00,0x00,0x00,0x00,0x00,0x17,0x01,0x01,0x04,0x92,0x00,0x00,0x00,0x54,0x80,0x00,0x00,0x00,0x00,0x00,0xe0,0x09,0x03,0x05,0x00,0x12,0x00,0x00,0x00,0xab,0x82,0x00,0x00,0x00,0x00,0x00,0x78,0x12,0x00,0x01,0x04,0x92,0x00,0x00,0xd0,0x25,0x54,0x00,0x00,0x00,0x00,0x00,0x1c,0x20,0x80,0x02,0x00,0x92,0x00,0x00,0x64,0x09,0x40,0x00,0x00,0x00,0x00,0x00,0x07,0x20,0x40,0x01,0x00,0x12,0x00,0x00,0x94,0x55,0x40,0x00,0x00,0x00,0x00,0xc0,0x01,0x40,0xb0,0x00,0x00,0x02,0x00,0x00,0x0e,0x05,0x41,0x00,0x00,0x00,0x00,0x78,0x00,0x80,0x4f,0x00,0x00,0x00,0x00,0x00,0x04,0x2b,0x44,0x00,0x00,0x00,0x80,0x07,0x00,0x00,0x10,0xb8,0x03,0x00,0x00,0x82,0x08,0x44,0x70,0x00,0x00,0x00,0x7c,0x00,0x00,0x00,0x00,0x40,0x01,0x00,0x80,0xbc,0x0a,0x1a,0x40,0x00,0x00,0xd0,0x01,0x00,0x00,0x00,0x00,0x40,0x00,0x00,0x20,0x55,0x15,0x14,0x21,0x00,0x00,0xa8,0x0a,0x00,0x00,0x00,0x00,0x40,0x00,0x00,0xe0,0x6f,0x3f,0x28,0x64,0x00,0x00,0x00,0xf0,0x0f,0x00,0x00,0x00,0x20,0x00,0x00,0x5c,0x54,0x00,0x50,0x20,0x00,0x00,0x00,0x00,0xf0,0x05,0x00,0x00,0x30,0x00,0x00,0x1a,0x58,0x00,0xa0,0x20,0x00,0x00,0x00,0x00,0x00,0xf8,0x0b,0x00,0x1e,0x00,0x00,0x04,0x40,0x00,0x40,0x21,0x00,0x00,0x00,0x00,0x00,0x00,0xe8,0xff,0x03,0x00,0x00,0x08,0x00,0x00,0x80,0x62,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0a,0x00,0x00,0x00,0x10,0x00,0x00,0x00,0x45,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xf8,0x05,0x00,0x00,0x20,0x00,0x00,0x00,0x4a,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3e,0x1c,0x00,0x40,0x00,0x00,0x00,0x58,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xe8,0xbf,0x00,0x80,0x01,0x00,0x00,0x50,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x90,0xd2,0x03,0x00,0x03,0x00,0x00,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x60,0x6a,0x3c,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x49,0x40,0xff,0x1b,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x96,0x01,0x00,0x7a,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xc0,0x07,0x00,0x00,0x30,0x7e,0x0c,0xbc,0x7f,0x00,0x00,0xc0,0x00,0x00,0x00,0x00,0xe0,0x07,0x00,0x00,0x70,0x66,0x06,0x7c,0xfe,0x01,0x00,0xc0,0x00,0x08,0x00,0x00,0x70,0x08,0x00,0x00,0x60,0x63,0x06,0x62,0xc6,0x01,0x00,0x00,0x00,0x0c,0x00,0x00,0x30,0xc0,0xc7,0xe6,0xe1,0xc3,0x02,0x60,0x86,0x81,0x1d,0xdb,0xec,0x3e,0x8f,0x0d,0x30,0xe0,0xcf,0xf7,0xc3,0xc1,0x03,0x3c,0x06,0x83,0x3f,0xdf,0xfc,0xbe,0x9f,0x0f,0x38,0x60,0xcc,0x30,0xc6,0x81,0x01,0x3c,0x86,0x81,0x71,0xc3,0xcc,0x88,0xb1,0x01,0x30,0x70,0xdc,0xf0,0x63,0x83,0x01,0x60,0x86,0x81,0x31,0xc3,0xcc,0xcc,0x9f,0x01,0x70,0x60,0xcc,0x30,0x60,0x87,0x01,0x62,0xc6,0x81,0x31,0xc3,0x8c,0x8c,0x81,0x01,0xe0,0xe7,0xcf,0xf0,0x33,0x86,0x01,0x7e,0xfe,0x80,0x3f,0xc3,0xcc,0xbc,0x9f,0x01,0xc0,0xc7,0xc3,0xe0,0x3b,0x8e,0x01,0x3e,0x7e,0x80,0x1d,0xc3,0x8c,0x39,0x9f,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 };
void loop() {
  String str = String(currentTemp) + "°C / " + String(targetTemp) + "°C";
  u8g2.clearBuffer();          // clear the internal memory
  //u8g2.setFont(u8g2_font_celibatemonk_tr); // choose a suitable font
  //u8g2.drawStr(0,20,str.c_str());  // write something to the internal memory
  u8g2.drawXBM(0,0,128,64,col);
  u8g2.sendBuffer();          // transfer internal memory to the display
  delay(1000);  

}
